package com.sunny.gateway.filter;

import cn.hutool.core.codec.Base64Encoder;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.sunny.gateway.dto.AuthResult;
import com.sunny.gateway.dto.Menu;
import com.sunny.gateway.exception.AccessDeniedException;
import com.sunny.gateway.exception.AuthenticationException;
import com.sunny.gateway.handler.MenuPermissionLoader;
import com.sunny.gateway.pms.PermissionService;
import com.sunny.gateway.pms.UserBanResolver;
import com.sunny.kratos.common.core.constant.CommonConstants;
import com.sunny.kratos.common.core.constant.SecurityConstants;
import com.sunny.kratos.common.core.constant.ServiceNameConstants;
import com.sunny.kratos.common.core.util.MsgUtils;
import com.sunny.kratos.common.core.util.WebUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.http.HttpStatus;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
import reactor.util.context.Context;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;

import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.addOriginalRequestUrl;

@Slf4j
public class SunnyRequestGlobalFilter implements GlobalFilter, Ordered {


	@Autowired
	private WebClient.Builder loadBalancedWebClientBuilder;

	@Autowired
	private UserBanResolver userBanResolver;
	// 将虚拟线程执行器和调度器作为单例管理
	private static final Executor VIRTUAL_THREAD_EXECUTOR = Executors.newVirtualThreadPerTaskExecutor();
	private static final Scheduler VIRTUAL_THREAD_SCHEDULER = Schedulers.fromExecutor(VIRTUAL_THREAD_EXECUTOR);



	@Override
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		ServerHttpRequest request = cleanRequestHeaders(exchange.getRequest());
		ServerHttpRequest newRequest = rewriteRequestPath(exchange, request);
		Route route = exchange.getRequiredAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
		String token = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
		if (isAuthRequired(route)) {
			return handleAuth(exchange, chain, newRequest, route, token);
		}
		return nextFilter(exchange, chain, newRequest);

	}

	private Mono<Void> nextFilter(ServerWebExchange exchange, GatewayFilterChain chain, ServerHttpRequest newRequest) {
		long startTime = Long.parseLong(newRequest.getHeaders().get(CommonConstants.REQUEST_START_TIME).getFirst());
		return chain.filter(exchange.mutate().request(newRequest).build()).doFinally(aVoid -> {
			long endTime = System.currentTimeMillis();
			log.info("Response URL: {}, Status Code: {}, Cost: {} ms",
					newRequest.getPath(),
					exchange.getResponse().getStatusCode().value(),
					endTime-startTime);
		});}

	private ServerHttpRequest cleanRequestHeaders(ServerHttpRequest request) {
		return request.mutate().headers(httpHeaders -> {
			httpHeaders.remove(SecurityConstants.FROM);
			httpHeaders.put(CommonConstants.REQUEST_START_TIME, Collections.singletonList(String.valueOf(System.currentTimeMillis())));
		}).build();
	}

	private ServerHttpRequest rewriteRequestPath(ServerWebExchange exchange, ServerHttpRequest request) {
		addOriginalRequestUrl(exchange, request.getURI());
		String newPath = "/" + Arrays.stream(StringUtils.tokenizeToStringArray(request.getURI().getRawPath(), "/"))
				.skip(1L)
				.collect(Collectors.joining("/"));
		return request.mutate().path(newPath).build();
	}

	private boolean isAuthRequired(Route route) {
		return "lb".equals(route.getUri().getScheme()) && !ServiceNameConstants.AUTH_SERVICE.equals(route.getUri().getHost());
	}

	private Mono<Void> handleAuth(ServerWebExchange exchange, GatewayFilterChain chain, ServerHttpRequest newRequest, Route route, String token) {
		Object menuDtoO = MenuPermissionLoader.menuData.get(route.getId());
		if (menuDtoO == null) {
			return nextFilter(exchange, chain, newRequest);
		}
		Menu menu = (Menu) menuDtoO;
		List<Menu.MenuDTO> menuDTOList = getMatchUrl(menu, newRequest.getMethod().name(), newRequest.getPath().toString());
		boolean needAuth = menuDTOList.stream().noneMatch(Menu.MenuDTO::isIgnoreAuth);
		return checkToken(token).publishOn(VIRTUAL_THREAD_SCHEDULER).flatMap(authResult -> {
			if (needAuth && !isAuthenticated(authResult)) {
				return Mono.error(new AuthenticationException(MsgUtils.getMessage("OAuth2ResourceOwnerBaseAuthenticationProvider.tokenExpired")));
			}

			ServerHttpRequest updatedRequest = updateRequestWithUser(newRequest, authResult.getPigUser());
			return checkPermissionIfRequired(menuDTOList, authResult.getPigUser())
					.flatMap(permissionGranted -> {
						if (!permissionGranted) {
							return Mono.error(new AccessDeniedException(MsgUtils.getMessage("AbstractAccessDecisionManager.accessDenied")));
						}
						return nextFilter(exchange, chain, updatedRequest);
					});
		}).onErrorResume(error-> Mono.error(error));
	}

	private boolean isAuthenticated(AuthResult authResult) {
		return authResult.getCode() == HttpStatus.OK.value() && authResult.getPigUser() != null;
	}

	private ServerHttpRequest updateRequestWithUser(ServerHttpRequest request, AuthResult.PigUser pigUser) {
		return request.mutate().headers(httpHeaders -> httpHeaders.put(CommonConstants.SUNNY_USER, Collections.singletonList(Base64Encoder.encode(JSON.toJSONString(pigUser))))).build();
	}

	private Mono<Boolean> checkPermissionIfRequired(List<Menu.MenuDTO> menuDTOList, AuthResult.PigUser pigUser) {
		if (menuDTOList.isEmpty() || StringUtils.isEmpty(menuDTOList.getFirst().getPreAuthorize())) {
			return Mono.just(true);
		}
		return checkPermission(menuDTOList.getFirst().getPreAuthorize(), pigUser);
	}

	private Mono<Boolean> checkPermission(String preAuthorize, AuthResult.PigUser pigUser) {
		SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
		StandardEvaluationContext context = new StandardEvaluationContext();
		context.setBeanResolver(userBanResolver);
		Context contextUser = Context.of(PermissionService.USER_KEY, pigUser);
		return Mono.defer(() -> spelExpressionParser.parseExpression(preAuthorize).getValue(context, Mono.class))
				.contextWrite(contextUser);
	}

	private Mono<AuthResult> checkToken(String token) {
		if (StringUtils.isEmpty(token) || token.length() <= 10 || !token.contains("Bearer")) {
			return Mono.just(new AuthResult(403, "token为空或数据不正确",null));
		}
		return Mono.justOrEmpty(token.substring(7)).flatMap(this::getUserById);
	}

	private Mono<AuthResult> getUserById(String token) {
		String url = "http://" + ServiceNameConstants.AUTH_SERVICE + "/token/check_token?token=" + token;
		return loadBalancedWebClientBuilder.build().get().uri(url)
				.retrieve()
				.bodyToMono(JSONObject.class)
				.map(this::parseAuthResult)
				.onErrorResume(error -> {
					log.error("error", error);
					return Mono.just(new AuthResult(403, "未登录",null));
				});
	}

	private AuthResult parseAuthResult(JSONObject jsonObject) {
		AuthResult authResult = new AuthResult();
		JSONObject userInfo = JSON.parseObject(JSONObject.toJSONString(jsonObject.get("user_info")));
		List<AuthResult.PigUser.Authority> authorities = userInfo.getJSONArray("authorities").toJavaList(AuthResult.PigUser.Authority.class);
		AuthResult.PigUser pigUser = new AuthResult.PigUser(userInfo.getLong("id"), userInfo.getLong("deptId"), userInfo.getString("name"), "", userInfo.getString("phone"), authorities);
		pigUser.setUsername(userInfo.getString("username"));
		pigUser.getAttributes().putAll(userInfo.getObject("attributes", Map.class));
		pigUser.getAttributes().put("clientId", jsonObject.get("clientId"));
		authResult.setPigUser(pigUser);
		authResult.setCode(200);
		return authResult;
	}

	private List<Menu.MenuDTO> getMatchUrl(Menu menu, String method, String url) {
		AntPathMatcher matcher = new AntPathMatcher();
		return menu.getMenuDTOList().stream()
				.filter(menuDTO -> matcher.match(url, menuDTO.getUrl()) && (menuDTO.getMethod().equals("*") || menuDTO.getMethod().equals(method)))
				.collect(Collectors.toList());
	}

	@Override
	public int getOrder() {
		return -1;
	}
}